// Copyright (C) Kumo inc. and its affiliates.
// Author: Jeff.li lijippy@163.com
// All rights reserved.
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
#pragma once

#include <boost/algorithm/string/case_conv.hpp>
#include <pollux/type/timestamp.h>
#include <pollux/core/query_config.h>
#include <pollux/expression/complex_view_types.h>
#include <pollux/external/date/date.h>
#include <pollux/external/date/iso_week.h>
#include <pollux/functions/macros.h>
#include <pollux/functions/lib/date_time_formatter.h>
#include <pollux/type/tz/time_zone_map.h>

namespace kumo::pollux::functions {
    inline constexpr int64_t kSecondsInDay = 86'400;
    inline constexpr int64_t kDaysInWeek = 7;
    extern const melon::F14FastMap<std::string, int8_t> kDayOfWeekNames;

    MELON_ALWAYS_INLINE const tz::TimeZone *getTimeZoneFromConfig(
        const core::QueryConfig &config) {
        if (config.adjustTimestampToTimezone()) {
            auto sessionTzName = config.sessionTimezone();
            if (!sessionTzName.empty()) {
                return tz::locateZone(sessionTzName);
            }
        }
        return nullptr;
    }

    MELON_ALWAYS_INLINE int64_t
    getSeconds(Timestamp timestamp, const tz::TimeZone *timeZone) {
        if (timeZone != nullptr) {
            timestamp.toTimezone(*timeZone);
            return timestamp.getSeconds();
        } else {
            return timestamp.getSeconds();
        }
    }

    MELON_ALWAYS_INLINE
    std::tm getDateTime(Timestamp timestamp, const tz::TimeZone *timeZone) {
        int64_t seconds = getSeconds(timestamp, timeZone);
        std::tm dateTime;
        POLLUX_USER_CHECK(
            Timestamp::epochToCalendarUtc(seconds, dateTime),
            "Timestamp is too large: {} seconds since epoch",
            seconds);
        return dateTime;
    }

    // days is the number of days since Epoch.
    MELON_ALWAYS_INLINE
    std::tm getDateTime(int32_t days) {
        int64_t seconds = days * kSecondsInDay;
        std::tm dateTime;
        POLLUX_USER_CHECK(
            Timestamp::epochToCalendarUtc(seconds, dateTime),
            "Date is too large: {} days",
            days);
        return dateTime;
    }

    MELON_ALWAYS_INLINE int getYear(const std::tm &time) {
        // tm_year: years since 1900.
        return 1900 + time.tm_year;
    }

    MELON_ALWAYS_INLINE int getMonth(const std::tm &time) {
        // tm_mon: months since January – [0, 11].
        return 1 + time.tm_mon;
    }

    MELON_ALWAYS_INLINE int getDay(const std::tm &time) {
        return time.tm_mday;
    }

    MELON_ALWAYS_INLINE int32_t getQuarter(const std::tm &time) {
        return time.tm_mon / 3 + 1;
    }

    MELON_ALWAYS_INLINE int32_t getDayOfYear(const std::tm &time) {
        return time.tm_yday + 1;
    }

    MELON_ALWAYS_INLINE uint32_t getWeek(
        const Timestamp &timestamp,
        const tz::TimeZone *timezone,
        bool allowOverflow) {
        // The computation of ISO week from date follows the algorithm here:
        // https://en.wikipedia.org/wiki/ISO_week_date
        Timestamp t = timestamp;
        if (timezone) {
            t.toTimezone(*timezone);
        }
        const auto timePoint = t.toTimePointMs(allowOverflow);
        const auto daysTimePoint = date::floor<date::days>(timePoint);
        const date::year_month_day calDate(daysTimePoint);
        auto weekNum = date::iso_week::year_weeknum_weekday{calDate}.weeknum();
        return (uint32_t) weekNum;
    }

    template<typename T>
    struct InitSessionTimezone {
        POLLUX_DEFINE_FUNCTION_TYPES(T);
        const tz::TimeZone *timeZone_{nullptr};

        MELON_ALWAYS_INLINE void initialize(
            const std::vector<TypePtr> & /*inputTypes*/,
            const core::QueryConfig &config,
            const arg_type<Timestamp> * /*timestamp*/) {
            timeZone_ = getTimeZoneFromConfig(config);
        }
    };

    /// Converts string as date time unit. Throws for invalid input string.
    ///
    /// @param unitString The input string to represent date time unit.
    /// @param throwIfInvalid Whether to throw an exception for invalid input
    /// string.
    /// @param allowMicro Whether to allow microsecond.
    /// @param allowAbbreviated Whether to allow abbreviated unit string.
    std::optional<DateTimeUnit> fromDateTimeUnitString(
        StringView unitString,
        bool throwIfInvalid,
        bool allowMicro = false,
        bool allowAbbreviated = false);

    /// Adjusts the given date time object to the start of the specified date time
    /// unit (e.g., year, quarter, month, week, day, hour, minute).
    void adjustDateTime(std::tm &dateTime, const DateTimeUnit &unit);

    /// Returns timestamp with seconds adjusted to the nearest lower multiple of the
    /// specified interval. If the given seconds is negative and not an exact
    /// multiple of the interval, it adjusts further down.
    MELON_ALWAYS_INLINE Timestamp
    adjustEpoch(int64_t seconds, int64_t intervalSeconds) {
        int64_t s = seconds / intervalSeconds;
        if (seconds < 0 && seconds % intervalSeconds) {
            s = s - 1;
        }
        int64_t truncatedSeconds = s * intervalSeconds;
        return Timestamp(truncatedSeconds, 0);
    }

    // Returns timestamp truncated to the specified unit.
    Timestamp truncateTimestamp(
        Timestamp timestamp,
        DateTimeUnit unit,
        const tz::TimeZone *timeZone);
} // namespace kumo::pollux::functions
